package p2p

import (
	"bufio"
	"crypto/ecdsa"
	"encoding/binary"
	"fmt"
	"time"

	"github.com/ethereum/go-ethereum/cmd/utils"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/p2p"
	"github.com/ethereum/go-ethereum/p2p/enode"
	"github.com/ethereum/go-ethereum/p2p/nat"
	"github.com/ethereum/go-ethereum/params"
	whisper "github.com/ethereum/go-ethereum/whisper/whisperv6"
	"github.com/hajimehoshi/oto"
	"github.com/prometheus/common/log"
	"gopkg.in/hraban/opus.v2"
)

type NameServer interface {
	Query(common.Address, bool) string
}

type NilName string

func (n NilName) Query(addr common.Address, b bool) string {
	return string(n) + ":" + addr.Hex()
}

type Config struct {
	EnodeString string
	// encryption
	AsymKey *ecdsa.PrivateKey
}

type P2PFMer struct {
	asymKey *ecdsa.PrivateKey
	// singletons
	server *p2p.Server
	shh    *whisper.Whisper
	done   chan struct{}
	input  *bufio.Reader
	output *bufio.Writer
	// topics
	topics  []Topic
	name    NameServer
	otoc    *oto.Context
	players map[common.Address]*Player
	counter int
}

type Topic struct {
	symKey   []byte
	topic    whisper.TopicType
	filterID string
	passed   string
}

type Player struct {
	dec    *opus.Decoder
	buf    []int16
	player *oto.Player
	ibuf   [][]byte
	last   int
}

func NewPlayer(ctx *oto.Context) *Player {
	const sampleRate = 48000
	const channels = 2
	dec, err := opus.NewDecoder(sampleRate, channels)
	if err != nil {
		log.Error(err)
		return nil
	}
	player := ctx.NewPlayer()
	frameSize := channels * 60 * sampleRate / 1000
	buf := make([]int16, int(frameSize))
	return &Player{dec, buf, player, make([][]byte, 16), 0}
}

func (player *Player) Play(din []byte) {
	at := uint(din[0]) & 0xf
	player.ibuf[at] = din
	for c := 0; player.ibuf[player.last&0xf] != nil && c < 2; c++ {
		pos := player.last & 0xf
		dii := player.ibuf[pos][1:]
		n, err := player.dec.Decode(dii, player.buf)
		if err != nil {
			log.Error(err)
		}
		player.ibuf[pos] = nil
		err = binary.Write(player.player, binary.LittleEndian, player.buf[:n*2])
		if err != nil {
			log.Error(err)
		}
		player.last += 1
	}
	if int(at) != player.last&0xf {
		// log.Info(player.last, at)
	}
}

func NewP2PFMer(config *Config, otoc *oto.Context, nameServer NameServer) (*P2PFMer, error) {
	ret := new(P2PFMer)

	ret.done = make(chan struct{})
	ret.asymKey = config.AsymKey

	peer := enode.MustParse(config.EnodeString)
	log.Info(peer)
	var peers = []*enode.Node{peer}
	for _, mainnode := range params.MainnetBootnodes {
		peer := enode.MustParse(mainnode)
		peers = append(peers, peer)
	}
	whisperConfig := whisper.DefaultConfig
	// whisperConfig.MinimumAcceptedPOW = 0.01
	whisperConfig.RestrictConnectionBetweenLightClients = true
	ret.shh = whisper.New(&whisperConfig)

	asymKeyID, err := ret.shh.AddKeyPair(ret.asymKey)
	if err != nil {
		return nil, fmt.Errorf("Failed to add a key pair: %s", err)
	}

	asymKey, err := ret.shh.GetPrivateKey(asymKeyID)
	if err != nil {
		return nil, fmt.Errorf("Failed to generate a new key pair: %s", err)
	}

	maxPeers := 10
	ret.server = &p2p.Server{
		Config: p2p.Config{
			PrivateKey:     asymKey,
			MaxPeers:       maxPeers,
			Name:           common.MakeName("p2p fm", "6.0"),
			Protocols:      ret.shh.Protocols(),
			NAT:            nat.Any(),
			BootstrapNodes: peers,
			StaticNodes:    peers,
			TrustedNodes:   peers,
		},
	}
	ret.name = nameServer
	ret.players = make(map[common.Address]*Player)
	ret.otoc = otoc
	return ret, nil
}

func (m *P2PFMer) AddToptic(topic, passwd string) error {
	var T Topic

	symKeyID, err := m.shh.AddSymKeyFromPassword(passwd)
	if err != nil {
		return fmt.Errorf("Failed to create symmetric key: %s", err)
	}
	T.symKey, err = m.shh.GetSymKey(symKeyID)
	T.passed = passwd
	copy(T.topic[:], common.FromHex(topic))

	filter := whisper.Filter{
		KeySym:   T.symKey,
		KeyAsym:  nil,
		Topics:   [][]byte{T.topic[:]},
		AllowP2P: true,
	}
	T.filterID, err = m.shh.Subscribe(&filter)
	if err != nil {
		return fmt.Errorf("Failed to install filter: %s", err)
	}

	m.topics = append(m.topics, T)

	return nil
}

func (m *P2PFMer) startServer() error {
	m.server.Start()
	return m.waitForConnection(true)
}

func (m *P2PFMer) waitForConnection(timeout bool) error {
	var cnt int
	var connected bool
	for !connected {
		time.Sleep(time.Millisecond * 500)
		connected = m.server.PeerCount() > 0
		if timeout {
			cnt++
			if cnt > 1000 {
				return fmt.Errorf("Timeout expired, failed to connect")
			}
		}
	}
	return nil
}

func (m *P2PFMer) Run(topic, passwd string) {
	err := m.startServer()
	if err != nil {
		fmt.Println(err)
		return
	}
	err = m.AddToptic(topic, passwd)
	if err != nil {
		fmt.Println(err)
		return
	}
	m.shh.Start(nil)
	finish := make(chan int)

	log.Info("Connected to peer.")
	go m.rxLoop(finish)
}

func (m *P2PFMer) Write(payload []byte) (n int, err error) {
	m.counter += 1
	msg := make([]byte, len(payload)+1)
	msg[0] = byte(m.counter)
	copy(msg[1:], payload)
	if true {
		topic := m.topics[0]
		params := whisper.MessageParams{
			Src:      m.asymKey,
			KeySym:   topic.symKey,
			Payload:  msg,
			Topic:    topic.topic,
			TTL:      whisper.DefaultTTL,
			PoW:      whisper.DefaultMinimumPoW,
			WorkTime: 15,
		}

		msg, err := whisper.NewSentMessage(&params)
		if err != nil {
			return 0, fmt.Errorf("failed to create new message: %s", err)
		}

		envelope, err := msg.Wrap(&params)
		if err != nil {
			return 0, fmt.Errorf("failed to seal message: %v \n", err)
		}

		err = m.shh.Send(envelope)
		if err != nil {
			return 0, fmt.Errorf("failed to send message: %v \n", err)
		}
	}
	return len(payload), nil
}

func (m *P2PFMer) Close() error {
	m.server.Stop()
	m.shh.Stop()
	m.done <- struct{}{}
	return nil
}

func (m *P2PFMer) rxLoop(finish chan int) {
	topic := m.topics[0]

	f := m.shh.GetFilter(topic.filterID)
	if f == nil {
		utils.Fatalf("filter is not installed")
	}

	ticker := time.NewTicker(time.Millisecond * 50)
	// c := 0
	for {
		select {
		case <-ticker.C:
			messages := f.Retrieve()
			// c += 1
			// if c%100 == 0 {
			// 	log.Info("in receive", len(messages))
			// }
			for _, msg := range messages {
				m.play(msg)
			}
		case <-m.done:
			finish <- 0
			return
		}
	}
}

func (m *P2PFMer) play(msg *whisper.ReceivedMessage) {
	buf := msg.Payload
	timestamp := time.Unix(int64(msg.Sent), 0).Format("2020-02-21 15:04:05")
	var address common.Address
	if msg.Src != nil {
		address = crypto.PubkeyToAddress(*msg.Src)
	}
	if whisper.IsPubKeyEqual(msg.Src, &m.asymKey.PublicKey) {
		return
	}
	if m.name == nil {
		if whisper.IsPubKeyEqual(msg.Src, &m.asymKey.PublicKey) {
			log.Debugf("\n%s <mine>: \n", timestamp) // message from myself
		} else {
			log.Debugf("\n%s [%x]: \n", timestamp, address) // message from a peer
		}
	} else {
		log.Debugf("\n%s [%s]: %s\n", timestamp, m.name.Query(address, false))
	}
	var ok bool
	var player *Player
	if player, ok = m.players[address]; !ok {
		log.Info("Install play", address)
		player = NewPlayer(m.otoc)
		m.players[address] = player
	}
	player.Play(buf)
}
